package net.protocol.diameter.avp;

import net.protocol.common.util.IPSubnet;
import net.protocol.common.util.Pair;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <pre>
 *    IPFilterRule
 *       The IPFilterRule format is derived from the OctetString AVP Base
 *       Format.  It uses the ASCII charset.  Packets may be filtered based
 *       on the following information that is associated with it:
 * 
 *          Direction                          (in or out)
 *          Source and destination IP ADDRESS_4_REGEX  (possibly masked)
 *          Protocol
 *          Source and destination port        (lists or ranges)
 *          TCP flags
 *          IP fragment flag
 *          IP options
 *          ICMP types
 * 
 *       Rules for the appropriate direction are evaluated in order, with
 *       the first matched rule terminating the evaluation.  Each packet is
 *       evaluated once.  If no rule matches, the packet is dropped if the
 *       last rule evaluated was a permit, and passed if the last rule was
 *       a deny.
 *       
 *       IPFilterRule filters MUST follow the format:
 * 
 *          action direction protocol from source to destination [options]
 * </pre>
 * 
 * @author jinhongw@gmail.com
 * @see <a href="http://tools.ietf.org/html/rfc3588#page-49">Diameter Base Protocol - IPFilterRule</a>
 * @see <a href="http://en.wikipedia.org/wiki/List_of_IP_protocol_numbers">List of IP protocol numbers</a>
 */
public class IPFilterRule extends OctetString {	
	
    /**
     * @param code AVP Code
     * @param rule AVP Data
     */
    public IPFilterRule(int code, String rule) {
    	super(code, rule);
    }
    
	/**
	 * @param code AVP Code
	 * @param vendorId Vendor-ID
	 * @param rule AVP Data
	 */
	public IPFilterRule(int code, int vendorId, String rule) {
		super(code, vendorId, rule);
	}
	
	/**
	 * @param code AVP Code
	 * @param rule AVP Data
	 */
	public IPFilterRule(int code, IPFilterRule.V rule) {
		this(code, DEFAULT_VENDOR_ID, rule);
	}

	/**
	 * @param code AVP Code
	 * @param vendorId Vendor-ID
	 * @param rule AVP Data
	 */
	public IPFilterRule(int code, int vendorId, IPFilterRule.V rule) {
		super(code, vendorId, rule.encode());
	}
	
	/**
	 * Creates a new <code>IPFilterRule</code> with the given DiameterAVPHeader
	 * and source ByteBuffer
	 * 
	 * @param header
	 *            Diameter AVP Header
	 * @param src
	 *            The buffer from which bytes are to be retrieved
	 */
	IPFilterRule(DiameterAVPHeader header, ByteBuffer src) {
		super(header, src);
	}

	@Override
	public final DataFormat getType() {
		return DataFormat.IPFilterRule;
	}

	@Override
	public String getName() {
		return DataFormat.IPFilterRule.name();
	}

	/**
	 * This class represents the Diameter IPFilterRule value
	 */
	public static class V {
		public final static int UNKOWN_PROTOCOL = -1;
		public String ANY_PROTOCOL = "ip";
		
		private Action action;
		private Direction direction;
		private boolean anyProtocol = false;
		private int protocol = UNKOWN_PROTOCOL;
		private Addresses source;
		private Addresses destination;
		private Options options;
		
		// action direction protocol from source to destination [options]
		final static String RULE_REGEX =
                "\\s*(?<action>[a-z]+)\\s+"         // action
				+ "(?<direction>[a-z]+)\\s+"        // direction
				+ "(?<protocol>\\w+)\\s+"			// protocol
				+ "from\\s+(?<source>.+)\\s+to"     // from source to
				+ "\\s+(?<destination>.+?)"         // destination
				+ "(?<options>(frag|ipoptions|tcpoptions|established|setup|tcpflags|icmptypes)(.*))?"; // options
		final static Pattern RULE_PATTERN = Pattern.compile(RULE_REGEX);

        final static String IPV4_REGEX = "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
        final static String IPV4_MASK_REGEX = "/\\s*" + IPV4_REGEX;
        final static String IPV6_REGEX = "((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))";
        final static String PREFIX_LENGTH_REGEX = "/\\s*" + "\\d{1,3}";

        final static String ADDRESS_4_REGEX =
                "("
                        + "(" + IPV4_REGEX + ")"
                        + "(" + IPV4_MASK_REGEX + ")?"      // network mask optional
                        + "(" + PREFIX_LENGTH_REGEX + ")?"  // prefix length optional
                        +
                ")";
        final static String ADDRESS_6_REGEX =
                "("
                        + "(" + IPV6_REGEX + ")"
                        + "(" + PREFIX_LENGTH_REGEX + ")?"  // prefix length optional
                        +
                 ")";

        final static String ADDRESS_REGEX = "(?<modifier>!?)\\s*"
                + "(?<address>any|assigned|"
                + ADDRESS_4_REGEX + "|"
                + ADDRESS_6_REGEX
                + ")"
                + "\\b\\s*"
                + "(?<ports>.*)?"; // port optional
        final static Pattern ADDRESS_PATTERN = Pattern.compile(ADDRESS_REGEX);
		
		private V(final String rule) {
			parse(rule);
		}
		
		/**
		 * Returns an {@code IPFilterRule.V} instance representing the specified
		 * rule String.
		 * 
		 * @param rule
		 *            a rule String
		 * @return an {@code IPFilterRule.V} instance representing rule.
		 */
		public static V of(final String rule) {
			return new V(rule);
		}

		private void parse(final String rule) {
			Matcher matcher = RULE_PATTERN.matcher(rule);
			if (matcher.matches()) {
				String action = matcher.group("action");
				parseAction(rule, action);
				
				String direction = matcher.group("direction");
				parseDirection(rule, direction);
				
				String protocol = matcher.group("protocol");
				parseProtocol(rule, protocol);
				
				String source = matcher.group("source");
                parseSource(rule, source);
				
				String destination = matcher.group("destination");
                parseDestination(rule, destination);
				
				String options = matcher.group("options");	
				parseOptions(rule, options);
			} else {
				throw new IllegalArgumentException("Could not parse rule {"
						+ rule + "}, failed to match.");
			}
		}
		
		public Action getAction() {
			return action;
		}

		public Direction getDirection() {
			return direction;
		}

		public boolean isAnyProtocol() {
			return anyProtocol;
		}

		public int getProtocol() {
			if (anyProtocol) return UNKOWN_PROTOCOL;
			return protocol;
		}

		public Addresses getSource() {
			return source;
		}

		public Addresses getDestination() {
			return destination;
		}

		public Options getOptions() {
			return options;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result
					+ ((action == null) ? 0 : action.hashCode());
			result = prime * result + (anyProtocol ? 1231 : 1237);
			result = prime * result
					+ ((destination == null) ? 0 : destination.hashCode());
			result = prime * result
					+ ((direction == null) ? 0 : direction.hashCode());
			result = prime * result
					+ ((options == null) ? 0 : options.hashCode());
			result = prime * result + protocol;
			result = prime * result
					+ ((source == null) ? 0 : source.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			V other = (V) obj;
			if (action != other.action)
				return false;
			if (anyProtocol != other.anyProtocol)
				return false;
			if (destination == null) {
				if (other.destination != null)
					return false;
			} else if (!destination.equals(other.destination))
				return false;
			if (direction != other.direction)
				return false;
			if (options == null) {
				if (other.options != null)
					return false;
			} else if (!options.equals(other.options))
				return false;
			if (protocol != other.protocol)
				return false;
			if (source == null) {
				if (other.source != null)
					return false;
			} else if (!source.equals(other.source))
				return false;
			return true;
		}

		/**
		 * <pre>
		 * The string returned is of the form:
		 *   action direction protocol from source to destination [options]
		 *   
		 *   For example:
		 *     permit in 17 from 192.168.5.27 8080 to any frag ipoptions rr tcpoptions mss,!ts,sack
		 * </pre>
		 * 
		 * @return a string representation of this <code>DiameterURI.V</code>
		 */
		@Override
		public String toString() {
			return encode();
		}

		private String encode() {
			StringBuilder buf = new StringBuilder();
			buf.append(action).append(" ");
			buf.append(direction).append(" ");
			if (anyProtocol) buf.append(ANY_PROTOCOL);
			else buf.append(protocol);
			buf.append(" from ").append(source);
			buf.append(" to ").append(destination);
            if (options != null)
                buf.append(" ").append(options);
			return buf.toString();
		}

		private void parseAction(String rule, String action) {
			this.action = Action.valueOf(action);
            if(this.action == null)
                throw new IllegalArgumentException(getMessage(rule, action));
		}
		
		private void parseDirection(String rule, String direction) {
			this.direction = Direction.valueOf(direction);
            if(this.direction == null)
                throw new IllegalArgumentException(getMessage(rule, direction));
		}
		
		private void parseProtocol(String rule, String protocol) {
			if (protocol.equalsIgnoreCase(ANY_PROTOCOL)) {
				anyProtocol = true;
			} else {
                try {
                    this.protocol = Integer.parseInt(protocol);
                } catch (NumberFormatException nfe) {
                    throw new IllegalArgumentException(getMessage(rule, protocol));
                }
            }
		}

        private void parseSource(String rule, String source) {
            this.source = getAddresses(rule, source);
        }

        private void parseDestination(String rule, String destination) {
            this.destination = getAddresses(rule, destination);
        }
        
        private void parseOptions(String rule, String options) {
        	this.options = getOptions(rule, options);
        }
		
		private Addresses getAddresses(String rule, String addresses) {
			if (addresses == null || addresses.equals("")) 
				throw new IllegalArgumentException(getMessage(rule, addresses));;
			Addresses o = new Addresses();
			Matcher matcher = ADDRESS_PATTERN.matcher(addresses);
			if (matcher.matches()) {
				String modifier = matcher.group("modifier");
				o.notMatch = (modifier == null ) ? false : modifier.equals("!");
				String address = matcher.group("address");
				o.representation = getRepresentation(rule, address);
				if (o.representation == Representation.ip) {
					try {
						if (address.indexOf("/") > 0) {
							o.ipSubnet = new IPSubnet(address);
						} else {
							o.address = Inet4Address.getByName(address);
						}
					} catch (UnknownHostException e) {
						String s = getMessage(rule,addresses, e.getMessage());
						throw new IllegalArgumentException(s);
					}
				}
				String ports = matcher.group("ports");
				o.ports = getPorts(rule, ports);
			} else {
				throw new IllegalArgumentException(getMessage(rule, addresses));
			}
			return o;
		}
		
		private Representation getRepresentation(String rule, String address) {
			if (address.equals(Representation.any.name())) {
				return Representation.any;
			} else if (address.equals(Representation.assigned.name())) {
				return Representation.assigned;
			} else {
				return Representation.ip;
			}
		}
		
		private Collection<Port> getPorts(String rule, String ports) {
			Collection<Port> set = new ArrayList<>();
			if (ports == null || ports.equals("")) return set;
			// 5061,5062,5068-5070
			String[] array = ports.split(",\\s*");
			for (String e : array) {
				try {
					int first = 0;
					int second = 0;	
					if (e.contains("-")) { // 
						String[] a = e.split("-\\s*"); 
						first = Integer.valueOf(a[0].trim());
						second = Integer.valueOf(a[1].trim());
					} else {
						int p = Integer.valueOf(e.trim());
						first = p;
						second = p;	
					}
					if (first == 0 || second == 0) continue;
					set.add(new Port(first, second));
				} catch (RuntimeException re) {
					throw new IllegalArgumentException(getMessage(rule, ports, re.getMessage()));
				}
			}
			return set;
		}
		
		private Options getOptions(String rule, String options) {
			if (options == null || options.equals("")) return null;
			Options o = new Options();
			String s = options.replaceAll("!\\s+", "!");
			s = s.replaceAll(",\\s+", ",");
			String[] a = s.split("\\s+");
			for (int i = 0; i < a.length; i++) {
				String e = a[i];
				switch (e) {
				case "frag":
					o.frag = true;
					break;
				case "setup":
					o.setup = true;
					break;
				case "established":
					o.established = true;
					break;
				case "ipoptions":
				case "tcpoptions":
				case "tcpflags":
				case "icmptypes":
					o.setOptions(a[++i], e);
					break;
				default:
					throw new IllegalArgumentException(getMessage(rule, options));
				}
			}
			return o;
		}
		
        private String getMessage(String rule, String error) {
            return "Could not parse rule {" + rule + "}, failed at {" + error + "}";
        }
        
        private String getMessage(String rule, String error,String exception) {
            return "Could not parse rule {" + rule + "}, failed at {" + error + "} " + exception;
        }
        
    	/**
    	 * <pre>
    	 * Action       
    	 *    permit - Allow packets that match the rule.
    	 *    deny   - Drop packets that match the rule.
    	 * </pre>
    	 */
    	public static enum Action { permit, deny }
    	
    	/**
    	 * <pre>
    	 * Direction          
    	 *     "in" is from the terminal, "out" is to the terminal.
    	 * </pre>
    	 */
    	public static enum Direction { in, out }

        /**
         * <pre>
         * Representation
         *     The keyword "any" is 0.0.0.0/0 or the IPv6 equivalent,
         *     The keyword "assigned" is the address or set of addresses assigned to the terminal,
         *     The "ip" means an IPv4 or IPv6 address.
         * </pre>
         */
        public static enum Representation { any, assigned, ip }
    	
    	/**
    	 * <pre>
    	 * ipoptions spec
    	 *     Match if the IP header contains the comma
    	 *     separated list of options specified in spec.  The
    	 *     supported IP options are:
    	 *        ssrr (strict source route), 
    	 *        lsrr (loose sourceroute), 
    	 *        rr (record packet route),
    	 *        ts(timestamp).  
    	 *     The absence of a particular option may be denoted with a '!'.
    	 */
    	public static enum IPOptions { ssrr, lsrr, rr, ts }
    	
    	/**
    	 * <pre>
    	 * tcpoptions spec
    	 *     Match if the TCP header contains the comma
    	 *     separated list of options specified in spec.  The
    	 *     supported TCP options are:
    	 *         mss (maximum segment size), 
    	 *         window (tcp window advertisement), 
    	 *         sack (selective ack), 
    	 *         ts (rfc1323 timestamp),
    	 *         cc (rfc1644 t/ tcp connection count).  
    	 *     The absence of a particular option may be denoted with a '!'.
    	 * </pre>
    	 */
    	public static enum TCPOptions { mss, window, sack, ts, cc }
    	
		/**
		 * <pre>
		 * tcpflags spec
		 *     TCP packets only.  Match if the TCP header
		 *     contains the comma separated list of flags
		 *     specified in spec.  The supported TCP flags are:
		 *          fin, syn, rst, psh, ack and urg.  
		 *     The absence of a particular flag may be denoted with a '!'. 
		 *     A rule that contains a tcpflags specification can never
		 *     match a fragmented packet that has a non-zero
		 *     offset.  See the frag option for details on
		 *     matching fragmented packets.
		 * </pre>
		 */
    	public static enum TCPFlags { fin, syn ,rst, psh, ack, urg }
    	
    	/**
    	 * <pre>
    	 * icmptypes types
    	 *     ICMP packets only.  Match if the ICMP type is in
    	 *     the list types.  The list may be specified as any
    	 *     combination of ranges or individual types
    	 *     separated by commas.  Both the numeric values and
    	 *     the symbolic values listed below can be used.  The
    	 *     supported ICMP types are:
    	 *         echo reply (0), 
    	 *         destination unreachable (3),
    	 *         source quench (4), 
    	 *         redirect (5), 
    	 *         echo request(8), 
    	 *         router advertisement (9), 
    	 *         router solicitation (10), 
    	 *         time-to-live exceeded (11), 
    	 *         IP header bad (12), 
    	 *         timestamp request (13),
    	 *         timestamp reply (14), 
    	 *         information request (15),
    	 *         information reply (16), 
    	 *         ADDRESS_4_REGEX mask request (17),
    	 *         ADDRESS_4_REGEX mask reply (18).
    	 * </pre>
    	 */
    	public static enum ICMPTypes {
    	    Echo_reply(0),
    	    Destination_unreachable(3),
    	    Source_quench(4),
    	    Redirect(5),
    	    Echo_request(8),
    	    Router_advertisement(9),
    	    Router_solicitation(10),
    	    Time_to_live_exceeded(11),
    	    IP_header_bad(12),
    	    Timestamp_request(13),
    	    Timestamp_reply(14),
    	    Information_request(15),
    	    Information_reply(16),
    	    Address_mask_request(17),
    	    Address_mask_reply(18);

    	    private final int value;
    	    
            private final static Map<Integer, ICMPTypes> KVS = new HashMap<Integer, ICMPTypes>();
            
            static {
                for (ICMPTypes e : values()) KVS.put(e.value, e);
            }
    	    
    		ICMPTypes(int value) {
    			this.value = value;
    		}

    		public int getValue() {
    			return value;
    		}
    		
            public static ICMPTypes valueOf(int value) {
                return KVS.get(value);
            }

            @Override
            public String toString() {
                String id = name();
                id = id.charAt(0) + id.substring(1).toLowerCase();
                return id + "(" + value + ")";
            }
    	}
    	
		/**
		 * <pre>
		 * If getRepresentation() == Representation.ip then return appropriate
		 * address: 
		 *     10.60.4.89 => getAddress() != null and getIPSubnet() == null
		 *     10.60.4.89/24 => getAddress() == null and getIPSubnet() != null 
		 *     10.60.4.89/255.255.255.0 => getAddress() == null and getIPSubnet() != null
		 *     !10.60.4.89 => isNotMatch() == true
		 *     10.60.4.89 => isNotMatch() == false
		 * Else return null.
		 * </pre>
		 */
    	public static class Addresses {
    		// ipno
    		InetAddress address;
    		// ipno/bits
    	    IPSubnet ipSubnet;
    		Collection<Port> ports = new ArrayList<>();;
    		Representation representation = Representation.ip;
    		
    		boolean notMatch = false;
    		
    		public InetAddress getAddress() {
    			return address;
    		}

    		public IPSubnet getIPSubnet() {
    			return ipSubnet;
    		}

			/**
			 * Returns an unmodifiable view of the specified ports.
			 * 
			 * @return an unmodifiable view of the specified ports.
			 */
			public Collection<Port> getPorts() {
				return Collections.unmodifiableCollection(ports);
			}
    		
    		public Representation getRepresentation() {
    			return representation;
    		}
    		
    		public boolean isIpv4() {
    			return address instanceof Inet4Address;
    		}

    		public boolean isIpv6() {
    			return address instanceof Inet6Address;
    		}
    	    
    		/**
    		 * The sense of the match can be inverted by preceding an address with
    		 * the not modifier (!), causing all other addresses to be matched
    		 * instead. This does not affect the selection of port numbers.
    		 * 
    		 * @return true if modifier is !
    		 */
    		public boolean isNotMatch() {
    			return notMatch;
    		}

    		@Override
			public int hashCode() {
				final int prime = 31;
				int result = 1;
				result = prime * result
						+ ((address == null) ? 0 : address.hashCode());
				result = prime * result
						+ ((ipSubnet == null) ? 0 : ipSubnet.hashCode());
				result = prime * result + (notMatch ? 1231 : 1237);
				result = prime * result
						+ ((ports == null) ? 0 : ports.hashCode());
				result = prime
						* result
						+ ((representation == null) ? 0 : representation
								.hashCode());
				return result;
			}

			@Override
			public boolean equals(Object obj) {
				if (this == obj)
					return true;
				if (obj == null)
					return false;
				if (getClass() != obj.getClass())
					return false;
				Addresses other = (Addresses) obj;
				if (address == null) {
					if (other.address != null)
						return false;
				} else if (!address.equals(other.address))
					return false;
				if (ipSubnet == null) {
					if (other.ipSubnet != null)
						return false;
				} else if (!ipSubnet.equals(other.ipSubnet))
					return false;
				if (notMatch != other.notMatch)
					return false;
				if (ports == null) {
					if (other.ports != null)
						return false;
				} else if (!ports.equals(other.ports))
					return false;
				if (representation != other.representation)
					return false;
				return true;
			}

			@Override
    		public String toString() {
    			StringBuilder buf = new StringBuilder();
    			if (notMatch) buf.append("!");
    			if (representation == Representation.ip) {
    				if (ipSubnet != null) buf.append(ipSubnet);
    				else buf.append(address.getHostAddress());
    			} else {
    				buf.append(representation.name());
    			}
    			if (!ports.isEmpty()) {
    				buf.append(" ");
    				for (Port e : ports) {
    					buf.append(e).append(",");
    				}
    				buf.deleteCharAt(buf.length() - 1);
    			}
    			return buf.toString();
    		}
    	}
    	
    	public static class Port  {
            public static final int MIN_PORT = 1;
            public static final int MAX_PORT = 65535;

            private final int first;
            private final int second;
            private final boolean isRange;
            
    		public Port(int first, int second) {
    			validate(first);
    			validate(second);
    			
    			this.first = first;
    			this.second = second;
    			isRange = (first != second);
    		}

    		public int getFirst() {
    			return first;
    		}

    		public int getSecond() {
    			return second;
    		}

    		public boolean isRange() {
    			return isRange; 
    		}
    		
    		@Override
    		public int hashCode() {
    			final int prime = 31;
    			int result = 1;
    			result = prime * result + first;
    			result = prime * result + second;
    			return result;
    		}

    		@Override
    		public boolean equals(Object obj) {
    			if (this == obj)
    				return true;
    			if (obj == null)
    				return false;
    			if (getClass() != obj.getClass())
    				return false;
    			Port other = (Port) obj;
    			if (first != other.first)
    				return false;
    			if (second != other.second)
    				return false;
    			return true;
    		}

    		@Override
    		public String toString() {
    			StringBuilder buf = new StringBuilder();
    			buf.append(first);
    			if (isRange) buf.append("-").append(second);
    			return buf.toString();
    		}

    		private void validate(int port) {
    			if (port < MIN_PORT || port > MAX_PORT)
    				throw new IllegalArgumentException("Invlid port:" + port);
    		}
        }
    	
		/**
		 * Collection&lt;Pair&lt;Boolean, Enum&gt;&gt; means that
		 * Collection&lt;Pair&lt;Modifier, Enum&gt;&gt; if this options with the
		 * not modifier(!) then modifier == true else modifier == false.
		 * <p>
		 * For example: ipoptions !rr,ts => Pair&lt;true, rr;&gt; and Pair&lt;false, ts;&gt;
		 */
		public static class Options {
			boolean frag;
			Collection<Pair<Boolean, IPOptions>> ipOptions = new ArrayList<>();
			Collection<Pair<Boolean, TCPOptions>> tcpOptions = new ArrayList<>();
			boolean established;
			boolean setup;
			Collection<Pair<Boolean, TCPFlags>> tcpFlags = new ArrayList<>();
			Collection<Pair<Boolean, ICMPTypes>> icmpTypes = new ArrayList<>();
			
			public boolean isFrag() {
				return frag;
			}
			
			/**
			 * Returns an unmodifiable view of {@link IPOptions} in this
			 * <code>Options</code>.
			 * 
			 * @return an unmodifiable view of <code>IPOptions</code> in this
			 *         <code>Options</code>.
			 */
			public Collection<Pair<Boolean, IPOptions>> getIpOptions() {
				return Collections.unmodifiableCollection(ipOptions);
			}

			/**
			 * Returns an unmodifiable view of {@link TCPOptions} in this
			 * <code>Options</code>.
			 * 
			 * @return an unmodifiable view of <code>TCPOptions</code> in this
			 *         <code>Options</code>.
			 */
			public Collection<Pair<Boolean, TCPOptions>> getTcpOptions() {
				return Collections.unmodifiableCollection(tcpOptions);
			}
			
			public boolean isEstablished() {
				return established;
			}
			
			public boolean isSetup() {
				return setup;
			}
			
			/**
			 * Returns an unmodifiable view of {@link TCPFlags} in this
			 * <code>Options</code>.
			 * 
			 * @return an unmodifiable view of <code>TCPFlags</code> in this
			 *         <code>Options</code>.
			 */
			public Collection<Pair<Boolean, TCPFlags>> getTcpFlags() {
				return Collections.unmodifiableCollection(tcpFlags);
			}
			
			/**
			 * Returns an unmodifiable view of {@link ICMPTypes} in this
			 * <code>Options</code>.
			 * 
			 * @return an unmodifiable view of <code>ICMPTypes</code> in this
			 *         <code>Options</code>.
			 */
			public Collection<Pair<Boolean, ICMPTypes>> getIcmpTypes() {
				return Collections.unmodifiableCollection(icmpTypes);
			}
			
			@Override
			public int hashCode() {
				final int prime = 31;
				int result = 1;
				result = prime * result + (established ? 1231 : 1237);
				result = prime * result + (frag ? 1231 : 1237);
				result = prime * result
						+ ((icmpTypes == null) ? 0 : icmpTypes.hashCode());
				result = prime * result
						+ ((ipOptions == null) ? 0 : ipOptions.hashCode());
				result = prime * result + (setup ? 1231 : 1237);
				result = prime * result
						+ ((tcpFlags == null) ? 0 : tcpFlags.hashCode());
				result = prime * result
						+ ((tcpOptions == null) ? 0 : tcpOptions.hashCode());
				return result;
			}

			@Override
			public boolean equals(Object obj) {
				if (this == obj)
					return true;
				if (obj == null)
					return false;
				if (getClass() != obj.getClass())
					return false;
				Options other = (Options) obj;
				if (established != other.established)
					return false;
				if (frag != other.frag)
					return false;
				if (icmpTypes == null) {
					if (other.icmpTypes != null)
						return false;
				} else if (!icmpTypes.equals(other.icmpTypes))
					return false;
				if (ipOptions == null) {
					if (other.ipOptions != null)
						return false;
				} else if (!ipOptions.equals(other.ipOptions))
					return false;
				if (setup != other.setup)
					return false;
				if (tcpFlags == null) {
					if (other.tcpFlags != null)
						return false;
				} else if (!tcpFlags.equals(other.tcpFlags))
					return false;
				if (tcpOptions == null) {
					if (other.tcpOptions != null)
						return false;
				} else if (!tcpOptions.equals(other.tcpOptions))
					return false;
				return true;
			}

			@Override
			public String toString() {
				StringBuilder buf = new StringBuilder();
				if (frag) buf.append("frag");
				if (!ipOptions.isEmpty()) {
                    buf.append(" ipoptions ");
					for (Pair<Boolean, IPOptions> e : ipOptions) {
                        if (e.getFirst()) buf.append("!");
						buf.append(e.getSecond()).append(",");
					}
					buf.deleteCharAt(buf.length()-1);
				}
                if (!tcpOptions.isEmpty()) {
                    buf.append(" tcpoptions ");
                    for (Pair<Boolean, TCPOptions> e : tcpOptions) {
                        if (e.getFirst()) buf.append("!");
                        buf.append(e.getSecond()).append(",");
                    }
                    buf.deleteCharAt(buf.length()-1);
                }
                if (established) buf.append(" established");
                if (setup) buf.append(" setup");
                if (!tcpFlags.isEmpty()) {
                    buf.append(" tcpflags ");
                    for (Pair<Boolean, TCPFlags> e : tcpFlags) {
                        if (e.getFirst()) buf.append("!");
                        buf.append(e.getSecond()).append(",");
                    }
                    buf.deleteCharAt(buf.length()-1);
                }
                if (!icmpTypes.isEmpty()) {
                    buf.append(" icmptypes ");
                    for (Pair<Boolean, ICMPTypes> e : icmpTypes) {
                        if (e.getFirst()) buf.append("!");
                        buf.append(e.getSecond()).append(",");
                    }
                    buf.deleteCharAt(buf.length()-1);
                }
				return buf.toString();
			}
			
			void setOptions(String v, String type) {
				String[] a = v.split(",");
				for (String e : a) {
                    String s = e;
                    boolean modifier = s.startsWith("!");
                    if (modifier) s = s.substring(1);
					switch (type) {
					case "ipoptions":
						ipOptions.add(Pair.of(modifier, IPOptions.valueOf(s)));
						break;
					case "tcpoptions":
						tcpOptions.add(Pair.of(modifier, TCPOptions.valueOf(s)));
						break;
					case "tcpflags":
						tcpFlags.add(Pair.of(modifier, TCPFlags.valueOf(s)));
						break;
					case "icmptypes":
						icmpTypes.add(Pair.of(modifier, ICMPTypes.valueOf(s)));
						break;
					default:
					}
				}
			}
		}
	}
}
